前言
使用Vue.js构建的单页面应用(SPA - single page application),需要引入很多的库,包括自家的router、vuex等,第三方的axios、loadash等等,不做任何配置打包之后的文件就有10M+,在第一次载入页面的时候需要加载完整的应用代码,会出现长时间的白屏,用户体验极差。
我们的目标就是减少包体积、提升加载速度,同时保证可用性、维护性、不侵入业务代码。
具体的优化方法包括:
诊断与验证
既然我们要做优化,我们必须先知道从哪些方面下手,最后如何去验证我们优化的效果。
- dist目录大小,最终打包的所有文件大小
- webpack-bundle-analyzer,分析包中包含的模块的大小
- 浏览器调试工具,开启Disable Cache,查看客户端加载的资源大小以及速度
主要介绍一下webpack-bundle-analyzer,看官方介绍:
This module will help you:
1.Realize what’s really inside your bundle
2.Find out what modules make up the most of its size
3.Find modules that got there by mistake
4.Optimize it!
效果图

使用方法
Vue CLI 3 默认支持打包报告,其实就是webpack-bundle-analyzer这个插件,使用vue-cli-service build --report就会在dist目录下生成一个report.html,打开这个页面就可以看到分析报告了。
为了方便使用,我们可以在package.json的scripts下面新增一行:"report": "vue-cli-service build --report",这样就可以使用npm run report命令实现了。
PS:老版本的vue cli可以通过npm run build --report命令,打包完成后会自动打开分析页面
怎么看这个报告呢?其实很简单,每一个分区代表打包以后的一个js文件,不同的颜色代表了文件的大小,从大到小,分区里面会嵌套分区,表示包对应的子模块;左侧的菜单展示了每个js文件在stat(原始)、parsed(编译后)、gizpped(压缩后)三种情况的大小。
优化前后对比
好不好,看疗效。我们先看结果再看过程。
优化前
- dist目录大小为13.3MB
- 打包后的本地js文件(/dist/static/js/): 
- 浏览器请求到的js文件: 
- webpack-bundle-analyzer:   
 可以看到,实际请求时需要加载两个本地js文件一共1.95M,还有一个第三方js文件815kb,合计2.74M,用时接近10s(吐槽下公司网络)。
优化后
- dist目录大小为2.95MB
- 打包后的本地js文件(/dist/static/js/): 
- 浏览器请求到的js文件: 
- webpack-bundle-analyzer:   
 可以看到,实际请求时请求了4个本地js文件一共156.5kb,CDN加载了6个包,一共207.5kb,合计364kb,用时2.2s,注意,优化后是包含了CDN资源的大小,网上很多网上都没有计算CDN的资源。对比优化前体积减少了87%,加载时间减少了78%(受网路影响,不太精确)。显而易见,优化效果非常显著,基本实现了秒开页面,用户体验杠杠滴!
优化过程
Vue Router 懒加载
首先看官方文档,是这样描述懒加载的
当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
实际操作非常简单,只需要把src/router.js中的组件引入方式从
| 1 | import Page404 from './src/Page404.vue'; | 
变更为1
const page404 = () => import('./views/404.vue');
就这样,over。当然,你想自定义分块也是可以的,通过 命名 chunk可以将多个组件打包到一个异步块中。
| 1 | const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') | 
最后的效果就是,打包后的js文件由之前的app.js和chunk-vendors.js多出了很多chunk,浏览器访问时会按需加载对应的chunk,从而减少了首屏的加载时间。
CDN引入依赖
CDN引入依赖很简单,使用script标签引入对应的资源即可,但是存在几个问题:
- CDN服务器挂了怎么办? - 要是我们能检测到CDN是否成功就好了,当然能。常用的依赖包都会创建一个全局变量,通过检测这个环节变量就可以知道CDN的资源是否加载成功,不成功就加载服务器资源就可以了。缺点就是服务器上必须存放一份依赖包的备份,会增加包体积,但是并不影响访问速度。 - 怎么知道依赖包的全局变量呢?我们用axios举例,打开axios的Github的dist目录,可以看到axios.js和axios.min.js,我们要使用的是min包,但我们要在axios.js中去查找他的全局变量。其中有这样一段代码。 - 1 
 2
 3
 4
 5
 6
 7
 8- if(typeof exports === 'object' && typeof module === 'object') 
 module.exports = factory();
 else if(typeof define === 'function' && define.amd)
 define([], factory);
 else if(typeof exports === 'object')
 exports["axios"] = factory();
 else
 root["axios"] = factory();- 我们就可以知道axios的全局变量就是axios了,在我们的publick/index.html中就可以这样检测 - 1 
 2
 3- <!--引入axios--> 
 <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
 <script>window.axios || document.write('<script src="<%= BASE_URL %>js/axios.min.js"><\/script>')</script>- 这样就实现了高可用的CDN引入依赖了,不怕挂! 
- 引入min包,没有了错误提醒,原有的业务代码中使用import引入的方式,CDN引入后无法使用了,维护两份代码? - 为了减小包体积提升速度,CDN需要引入min包,但是就损失依赖自带的错误提醒,只适用于生产环境,传统的做法就是维护两分代码,开发版使用完整包 或者import引入,生产环境引入min包,这对于一个懒人来说太麻烦了。 - 要是能这样就好了,业务代码使用import方式引入依赖,开发环境加载完整包,生产环境使用CDN加载min包,完美!但是,这可能吗?当然可能,使用webpack的externals+html-webpack-plugin就可以轻松搞定啦! - externals的作用就是在使用CDN的情况下,业务代码中仍然可以使用import引入依赖。官方文档是这样说的 - 防止将某些 - import的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。- externals在vue.config.js中的配置webpack - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11- configureWebpack: config => { 
 if (process.env.NODE_ENV === 'production') {
 config.externals = {
 'vue': 'Vue',
 'vuex': 'Vuex',
 'vue-router': 'VueRouter',
 'axios': 'axios',
 'vue-grid-layout': 'VueGridLayout'
 };
 }
 }- 配置的格式: - import包名: 全局变量,工作原理就是webpack在读取到import语句时会从externals中去查找对应的全局变量然后加载。比如- 'vue-router': 'VueRouter',业务代码中使用- import router from 'vue-router',webpack就会去查找- VueRouter这个全局变量。- 通过 - process.env.NODE_ENV === 'production'判断生产环境才配置externals,开发环境仍然加载- npm install安装的依赖,这样就解决了生产环境使用import引入读取CDN资源的问题,但是开发环境CDN资源仍然会加载,我们并不会用到,这种浪费浏览器资源的事情是不能容忍的,必须干掉!这时候就需要html-webpack-plugin登场了。- html-webpack-plugin在Vue CLI 中主要负责处理public/index.html,具体查看文档,在public/index.html中判断运行环境决定是否启用CDN。 - 1 
 2
 3
 4
 5- <% if (htmlWebpackPlugin.options.environment === 'production') { %> 
 <!--引入axios-->
 <script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"></script>
 <script>window.axios || document.write('<script src="<%= BASE_URL %>js/axios.min.js"><\/script>')</script>
 <% } %>- 这个 - environment并不是自带的,需要我们再vue.config.js中进行配置- 1 
 2
 3
 4
 5
 6
 7
 8
 9- // 在htmlWebpackPlugin中增加环境变量,在index.html中使用 
 chainWebpack: config => {
 config
 .plugin('html')
 .tap(args => {
 args[0].environment = process.env.NODE_ENV;
 return args;
 });
 }- 至此,就实现高可用CDN的工程化了,不需要改业务代码,不需要开发环境生产环境两套代码,三分钟解决烦恼! 
第三方库按需引入
都有了CDN了,为什么还需要按需引入呢?举个例子,项目中需要使用lodash的deepClone和isEqual两个方法,使用前面的方法引入loadash,虽然有CDN的加持,但是我们仍然需要加载lodash的完整包,为了两个方法就要加载完整包还是有些得不偿失的,这种情况按需引入可以获得更快的加载速度。
具体按需引入的方法,echarts、element-ui等官方文档都有说明,照着操作就可以了。
lodash的按需引入稍微麻烦一些,需要安装babel-plugin-lodash这个包
| 1 | npm install babel-plugin-lodash -D | 
然后配置babel.config.js
| 1 | module.exports = { | 
引用部分
| 1 | import lodash from 'lodash'; | 
只有通过lodash的.运算符调用的方法才会被引入
开启Gzip
nginx的Gzip有两种方式,一种是服务器端的Gzip,这个大家都知道,就是每次请求时服务器先压缩再返回资源,对服务器性能有一定消耗;另一种是Gzip_static,就是打包时生成.gz文件,每次请求时服务器直接返回.gz文件,不消耗服务器性能。两种开启一种就可以了。
- 服务器端Gzip - 很简单,只需要配置一下nginx的配置就可以了 - 1 
 2
 3
 4
 5
 6
 7
 8- # 开启gzip 
 gzip on;
 # gzip 压缩级别,1-10,数字越大压缩的越好,也越占用CPU
 gzip_comp_level 5;
 # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩
 gzip_min_length 256;
 # 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到
 gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript;
- Gzip_static - 需要安装compression-webpack-plugin实现打包为.gz文件,然后开启nginx的gzip_static让nginx优先查找文件的.gz版本发送给客户端。 - 1 - npm install compression-webpack-plugin -D - vue.config.js中配置webpack - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12- const productionGzipExtensions = ['js', 'css'] 
 configureWebpack: {
 plugins: [
 new CompressionWebpackPlugin({
 asset: '[path].gz[query]',
 algorithm: 'gzip',
 test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
 threshold: 10240,
 minRatio: 0.8
 })
 ]
 }- nginx配置 - 1 
 2- # 开启gzip_static 
 gzip_static on;
其他配置
- 配置vue.config.js关闭生成环境的sourcemap - 1 - productionSourceMap: false 
- 减少不必要的第三方依赖 
- 安装依赖区分开发模式,只在开发模式使用的依赖使用dev模式安装,例如 - 1 - npm install xxx --save-dev 
总结
优化是没有极限的,不同的项目需要采用不同的优化方案,没有最好的,只有最合适的。填坑之路很漫长,但是我相信,办法总比困难多。